// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <assert.h>
#include <fcntl.h>
#include <lib/gfx-font-data/gfx-font-data.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <zircon/process.h>
#include <zircon/syscalls.h>

#include <fbl/auto_lock.h>

#include "vc.h"

// Default height/width (in px) of console before any displays are
// attached, since we need somewhere to put any data that is received.
#define DEFAULT_WIDTH 1024
#define DEFAULT_HEIGHT 768
#define SCROLLBACK_ROWS 1024  // TODO make configurable

#define ABS(val) (((val) >= 0) ? (val) : -(val))

static zx_status_t vc_setup(vc_t* vc, const color_scheme_t* color_scheme) {
  // calculate how many rows/columns we have
  vc->rows = DEFAULT_HEIGHT / vc->charh;
  vc->columns = DEFAULT_WIDTH / vc->charw;
  vc->scrollback_rows_max = SCROLLBACK_ROWS;
  vc->scrollback_rows_count = 0;
  vc->scrollback_offset = 0;

  // allocate the text buffer
  vc->text_buf =
      reinterpret_cast<vc_char_t*>(calloc(1, vc->rows * vc->columns * sizeof(vc_char_t)));
  if (!vc->text_buf)
    return ZX_ERR_NO_MEMORY;

  // allocate the scrollback buffer
  vc->scrollback_buf = reinterpret_cast<vc_char_t*>(
      calloc(1, vc->scrollback_rows_max * vc->columns * sizeof(vc_char_t)));
  if (!vc->scrollback_buf) {
    free(vc->text_buf);
    return ZX_ERR_NO_MEMORY;
  }

  // set up the default palette
  memcpy(&vc->palette, default_palette, sizeof(default_palette));

  vc->front_color = color_scheme->front;
  vc->back_color = color_scheme->back;

  return ZX_OK;
}

bool vc_graphics_enabled(vc_t* vc) { return vc->graphics && vc->graphics->vc_gfx; }

static void vc_invalidate(void* cookie, int x0, int y0, int w, int h) {
  vc_t* vc = reinterpret_cast<vc_t*>(cookie);

  if (!g_vc_owns_display || !vc->active || !vc_graphics_enabled(vc)) {
    return;
  }

  assert(h >= 0);
  int y1 = y0 + h;
  assert(y0 <= static_cast<int>(vc->rows));
  assert(y1 <= static_cast<int>(vc->rows));

  // Clip the y range so that we don't unnecessarily draw characters
  // outside the visible range, and so that we don't draw characters into
  // the bottom margin.
  int visible_y0 = vc->viewport_y;
  int visible_y1 = vc->viewport_y + vc_rows(vc);
  y0 = MAX(y0, visible_y0);
  y1 = MIN(y1, visible_y1);

  for (int y = y0; y < y1; y++) {
    if (y < 0) {
      // Scrollback row.
      vc_char_t* row = vc_get_scrollback_line_ptr(vc, y + vc->scrollback_rows_count);
      for (int x = x0; x < x0 + w; x++) {
        vc_gfx_draw_char(vc->graphics, vc, row[x], x, y - vc->viewport_y,
                         /* invert= */ false);
      }
    } else {
      // Row in the main console region (non-scrollback).
      vc_char_t* row = &vc->text_buf[y * vc->columns];
      for (int x = x0; x < x0 + w; x++) {
        // Check whether we should display the cursor at this
        // position.  Note that it's possible that the cursor is
        // outside the display area (vc->cursor_x ==
        // vc->columns).  In that case, we won't display the
        // cursor, even if there's a margin.  This matches
        // gnome-terminal.
        bool invert = (!vc->hide_cursor && static_cast<unsigned>(x) == vc->cursor_x &&
                       static_cast<unsigned>(y) == vc->cursor_y);
        vc_gfx_draw_char(vc->graphics, vc, row[x], x, y - vc->viewport_y, invert);
      }
    }
  }
}

// implement tc callbacks:

static inline void vc_invalidate_lines(vc_t* vc, int y, int h) {
  if (y < vc->invy0) {
    vc->invy0 = y;
  }
  y += h;
  if (y > vc->invy1) {
    vc->invy1 = y;
  }
}

static void vc_tc_invalidate(void* cookie, int x0, int y0, int w, int h) {
  vc_t* vc = reinterpret_cast<vc_t*>(cookie);
  vc_invalidate(cookie, x0, y0, w, h);
  vc_invalidate_lines(vc, y0, h);
}

static void vc_tc_movecursor(void* cookie, int x, int y) {
  vc_t* vc = reinterpret_cast<vc_t*>(cookie);
  unsigned old_x = vc->cursor_x;
  unsigned old_y = vc->cursor_y;
  vc->cursor_x = x;
  vc->cursor_y = y;
  if (g_vc_owns_display && vc->active && !vc->hide_cursor) {
    // Clear the cursor from its old position.
    vc_invalidate(cookie, old_x, old_y, 1, 1);
    vc_invalidate_lines(vc, old_y, 1);

    // Display the cursor in its new position.
    vc_invalidate(cookie, vc->cursor_x, vc->cursor_y, 1, 1);
    vc_invalidate_lines(vc, vc->cursor_y, 1);
  }
}

static void vc_tc_scrollback_buffer_push(vc_t* vc, vc_char_t* src) {
  unsigned dest_row;
  assert(vc->scrollback_rows_count <= vc->scrollback_rows_max);
  if (vc->scrollback_rows_count < vc->scrollback_rows_max) {
    // Add a row without dropping any existing rows.
    assert(vc->scrollback_offset == 0);
    dest_row = vc->scrollback_rows_count++;
  } else {
    // Add a row and drop an existing row.
    assert(vc->scrollback_offset < vc->scrollback_rows_max);
    dest_row = vc->scrollback_offset++;
    if (vc->scrollback_offset == vc->scrollback_rows_max)
      vc->scrollback_offset = 0;
  }
  vc_char_t* dst = &vc->scrollback_buf[dest_row * vc->columns];
  memcpy(dst, src, vc->columns * sizeof(vc_char_t));
}

static void vc_tc_push_scrollback_line(void* cookie, int y) {
  vc_t* vc = reinterpret_cast<vc_t*>(cookie);
  vc_char_t* src = &vc->text_buf[y * vc->columns];
  vc_tc_scrollback_buffer_push(vc, src);

  // If we're displaying only the main console region (and no
  // scrollback), then keep displaying that (i.e. don't modify
  // viewport_y).
  if (vc->viewport_y < 0) {
    // We are displaying some of the scrollback buffer.
    if (vc->viewport_y > -static_cast<int>(vc->scrollback_rows_max)) {
      // Scroll the viewport to continue displaying the same point in
      // the scrollback buffer.
      --vc->viewport_y;
    } else {
      // We were displaying the line at the top of the scrollback
      // buffer, but we dropped that line from the buffer.  We could
      // leave the display as it was (which is what gnome-terminal
      // does) and not scroll the display.  However, that causes
      // problems.  If the user later scrolls down, we won't
      // necessarily be able to display the lines below -- we might
      // have dropped those too.  So, instead, let's scroll the
      // display and remove the scrollback line that was lost.
      //
      // For simplicity, fall back to redrawing everything.
      vc_invalidate(vc, 0, -vc->scrollback_rows_max, vc->columns, vc_rows(vc));
      vc_render(vc);
    }
  }
}

static void vc_set_cursor_hidden(vc_t* vc, bool hide) {
  if (vc->hide_cursor == hide)
    return;
  vc->hide_cursor = hide;
  if (g_vc_owns_display && vc->active) {
    vc_invalidate(vc, vc->cursor_x, vc->cursor_y, 1, 1);
    vc_invalidate_lines(vc, vc->cursor_y, 1);
  }
}

static void vc_tc_copy_lines(void* cookie, int y_dest, int y_src, int line_count) {
  vc_t* vc = reinterpret_cast<vc_t*>(cookie);

  if (vc->viewport_y < 0) {
    tc_copy_lines(&vc->textcon, y_dest, y_src, line_count);

    // The viewport is scrolled.  For simplicity, fall back to
    // redrawing all of the non-scrollback lines in this case.
    int rows = vc_rows(vc);
    vc_invalidate(vc, 0, 0, vc->columns, rows);
    vc_invalidate_lines(vc, 0, rows);
    return;
  }

  // Remove the cursor from the display before copying the lines on
  // screen, otherwise we might be copying a rendering of the cursor to a
  // position where the cursor isn't.  This must be done before the
  // tc_copy_lines() call, otherwise we might render the wrong character.
  bool old_hide_cursor = vc->hide_cursor;
  if (g_vc_owns_display && vc->active) {
    vc_set_cursor_hidden(vc, true);
  }

  // The next two calls can be done in any order.
  tc_copy_lines(&vc->textcon, y_dest, y_src, line_count);

  if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) {
    gfx_copyrect(vc->graphics->vc_gfx, 0, y_src * vc->charh, vc->graphics->vc_gfx->width,
                 line_count * vc->charh, 0, y_dest * vc->charh);

    // Restore the cursor.
    vc_set_cursor_hidden(vc, old_hide_cursor);

    vc_status_update();
    vc_gfx_invalidate_status(vc->graphics);
    vc_invalidate_lines(vc, 0, vc_rows(vc));
  }
}

static void vc_tc_setparam(void* cookie, int param, uint8_t* arg, size_t arglen) {
  vc_t* vc = reinterpret_cast<vc_t*>(cookie);
  switch (param) {
    case TC_SET_TITLE:
      strncpy(vc->title, (char*)arg, sizeof(vc->title));
      vc->title[sizeof(vc->title) - 1] = '\0';
      vc_status_update();
      if (g_vc_owns_display && vc_graphics_enabled(vc)) {
        vc_gfx_invalidate_status(vc->graphics);
      }
      break;
    case TC_SHOW_CURSOR:
      vc_set_cursor_hidden(vc, false);
      break;
    case TC_HIDE_CURSOR:
      vc_set_cursor_hidden(vc, true);
      break;
    default:;  // nothing
  }
}

static void vc_clear_gfx(vc_t* vc) {
  // Fill display with background color
  if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) {
    gfx_fillrect(vc->graphics->vc_gfx, 0, 0, vc->graphics->vc_gfx->width,
                 vc->graphics->vc_gfx->height, palette_to_color(vc, vc->back_color));
  }
}

static void vc_reset(vc_t* vc) {
  // reset the cursor
  vc->cursor_x = 0;
  vc->cursor_y = 0;
  // reset the viewport position
  vc->viewport_y = 0;

  tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf, vc->front_color, vc->back_color,
          vc->cursor_x, vc->cursor_y);
  vc->textcon.cookie = vc;
  vc->textcon.invalidate = vc_tc_invalidate;
  vc->textcon.movecursor = vc_tc_movecursor;
  vc->textcon.push_scrollback_line = vc_tc_push_scrollback_line;
  vc->textcon.copy_lines = vc_tc_copy_lines;
  vc->textcon.setparam = vc_tc_setparam;

  // fill textbuffer with blank characters
  size_t count = vc->rows * vc->columns;
  vc_char_t* ptr = vc->text_buf;
  while (count--) {
    *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
  }

  vc_clear_gfx(vc);
  if (vc_graphics_enabled(vc)) {
    vc_gfx_invalidate_all(vc->graphics, vc);
  }
}

void vc_status_clear() {
  if (g_vc_owns_display && g_active_vc && vc_graphics_enabled(g_active_vc)) {
    gfx_fillrect(g_active_vc->graphics->vc_status_bar_gfx, 0, 0,
                 g_active_vc->graphics->vc_status_bar_gfx->width,
                 g_active_vc->graphics->vc_status_bar_gfx->height,
                 default_palette[STATUS_COLOR_BG]);
  }
}

void vc_status_commit() {
  if (g_vc_owns_display && g_active_vc && vc_graphics_enabled(g_active_vc)) {
    vc_gfx_invalidate_status(g_active_vc->graphics);
  }
}

void vc_status_write(int x, unsigned color, const char* text) {
  char c;
  unsigned fg = default_palette[color];
  unsigned bg = default_palette[STATUS_COLOR_BG];
  vc_t* vc = g_active_vc;
  if (!g_active_vc) {
    return;
  }

  if (g_vc_owns_display && vc_graphics_enabled(vc)) {
    x *= vc->graphics->vc_font->width;
    while ((c = *text++) != 0) {
      gfx_putchar(vc->graphics->vc_status_bar_gfx, vc->graphics->vc_font, c, x, 0, fg, bg);
      x += vc->graphics->vc_font->width;
    }
  }
}

void vc_render(vc_t* vc) {
  if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) {
    vc_status_update();
    vc_gfx_invalidate_all(vc->graphics, vc);
  }
}

void vc_full_repaint(vc_t* vc) {
  if (g_vc_owns_display && vc_graphics_enabled(vc)) {
    vc_clear_gfx(vc);
    int scrollback_lines = vc_get_scrollback_lines(vc);
    vc_invalidate(vc, 0, -scrollback_lines, vc->columns, scrollback_lines + vc->rows);
  }
}

int vc_get_scrollback_lines(vc_t* vc) { return vc->scrollback_rows_count; }

vc_char_t* vc_get_scrollback_line_ptr(vc_t* vc, unsigned row) {
  assert(row < vc->scrollback_rows_count);
  row += vc->scrollback_offset;
  if (row >= vc->scrollback_rows_max)
    row -= vc->scrollback_rows_max;
  return &vc->scrollback_buf[row * vc->columns];
}

static void vc_scroll_viewport_abs(vc_t* vc, int vpy) {
  vpy = MIN(vpy, 0);
  vpy = MAX(vpy, -vc_get_scrollback_lines(vc));
  int diff = vpy - vc->viewport_y;
  if (diff == 0)
    return;
  int diff_abs = ABS(diff);
  vc->viewport_y = vpy;
  int rows = vc_rows(vc);
  if (!g_vc_owns_display || !vc->active || !vc_graphics_enabled(vc)) {
    return;
  }
  if (diff_abs >= rows) {
    // We are scrolling the viewport by a large delta.  Invalidate all
    // of the visible area of the console.
    vc_invalidate(vc, 0, vpy, vc->columns, rows);
  } else {
    if (diff > 0) {
      gfx_copyrect(vc->graphics->vc_gfx, 0, diff_abs * vc->charh, vc->graphics->vc_gfx->width,
                   (rows - diff_abs) * vc->charh, 0, 0);
      vc_invalidate(vc, 0, vpy + rows - diff_abs, vc->columns, diff_abs);
    } else {
      gfx_copyrect(vc->graphics->vc_gfx, 0, 0, vc->graphics->vc_gfx->width,
                   (rows - diff_abs) * vc->charh, 0, diff_abs * vc->charh);
      vc_invalidate(vc, 0, vpy, vc->columns, diff_abs);
    }
  }
  vc_render(vc);
}

void vc_scroll_viewport(vc_t* vc, int dir) { vc_scroll_viewport_abs(vc, vc->viewport_y + dir); }

void vc_scroll_viewport_top(vc_t* vc) { vc_scroll_viewport_abs(vc, INT_MIN); }

void vc_scroll_viewport_bottom(vc_t* vc) { vc_scroll_viewport_abs(vc, 0); }

void vc_set_fullscreen(vc_t* vc, bool fullscreen) {
  unsigned flags;
  if (fullscreen) {
    flags = vc->flags | VC_FLAG_FULLSCREEN;
  } else {
    flags = vc->flags & ~VC_FLAG_FULLSCREEN;
  }
  if (flags != vc->flags) {
    vc->flags = flags;
    tc_seth(&vc->textcon, vc_rows(vc));
  }
  vc_render(vc);
}

const gfx_font* vc_get_font() {
  char* fname = getenv("virtcon.font");
  if (fname) {
    if (!strcmp(fname, "9x16")) {
      return &gfx_font_9x16;
    } else if (!strcmp(fname, "18x32")) {
      return &gfx_font_18x32;
    } else {
      printf("gfxconsole: no such font '%s'\n", fname);
    }
  }
  return &gfx_font_9x16;
}

void vc_attach_gfx(vc_t* vc) {
  if (vc->graphics == nullptr) {
    return;
  }
  // If the size of the new gfx console doesn't match what we had been
  // attached to, we need to allocate new memory and copy the existing
  // data over.
  unsigned rows = vc->graphics->vc_gfx->height / vc->charh;
  unsigned columns = vc->graphics->vc_gfx->width / vc->charw;
  if (rows == vc->rows && columns == vc->columns) {
    return;
  }

  // allocate the new buffers
  vc_char_t* text_buf = reinterpret_cast<vc_char_t*>(calloc(1, rows * columns * sizeof(vc_char_t)));
  vc_char_t* scrollback_buf = reinterpret_cast<vc_char_t*>(
      calloc(1, vc->scrollback_rows_max * columns * sizeof(vc_char_t)));
  if (text_buf && scrollback_buf) {
    // fill new text buffer with blank characters
    size_t count = rows * columns;
    vc_char_t* ptr = text_buf;
    while (count--) {
      *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
    }

    // Copy the most recent data from the old console to the new one. There are
    // (vc->cursor_y + 1) rows available, and we want (rows - (vc->rows - vc_rows(vc))
    // rows. Subtract to get the first row index to copy.
    unsigned old_i =
        MAX(static_cast<int>((vc->cursor_y + 1) - (rows - (vc->rows - vc_rows(vc)))), 0);
    unsigned old_data_start = old_i;
    unsigned new_i = 0;
    size_t len = (vc->columns < columns ? vc->columns : columns) * sizeof(vc_char_t);
    while (new_i < rows && old_i <= vc->cursor_y) {
      memcpy(text_buf + columns * (new_i++), vc->text_buf + vc->columns * (old_i++), len);
    }

    // copy the old scrollback buffer
    for (int i = 0; i < SCROLLBACK_ROWS; i++) {
      memcpy(scrollback_buf + columns * i, vc->scrollback_buf + vc->columns * i, len);
    }

    vc_char_t* old_text_buf = vc->text_buf;
    unsigned old_columns = vc->columns;
    free(vc->scrollback_buf);

    vc->text_buf = text_buf;
    vc->scrollback_buf = scrollback_buf;
    vc->rows = rows;
    vc->columns = columns;

    // Push any data that fell off of text_buf. Use a temporary buffer of the
    // right length to handle going to a wider console. Set it to ' 's before
    // pushing, so we don't merge data from old rows.
    if (old_data_start) {
      vc_char_t buf[columns];
      for (unsigned i = 0; i < old_data_start; i++) {
        vc_char_t* ptr = buf;
        while (ptr < buf + columns) {
          *ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
        }
        ptr = old_text_buf + i * old_columns;
        memcpy(buf, ptr, len);

        vc_tc_scrollback_buffer_push(vc, buf);
      }
    }

    free(old_text_buf);
  } else {
    // If we failed to allocate new buffers, use the old ones as best we can
    free(text_buf);
    free(scrollback_buf);

    vc->rows = MIN(vc->rows, rows);
    vc->columns = MIN(vc->columns, columns);

    printf("vc: buffer resize failed, reusing old buffers (%dx%d)\n", vc->rows, vc->columns);
  }

  vc->viewport_y = 0;
  if (vc->cursor_x >= vc->columns) {
    vc->cursor_x = vc->columns - 1;
  }
  if (static_cast<int>(vc->cursor_y) >= vc_rows(vc)) {
    vc->cursor_y = vc_rows(vc) - 1;
  }

  tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf, vc->front_color, vc->back_color,
          vc->cursor_x, vc->cursor_y);
}

zx_status_t vc_alloc(vc_t** out, const color_scheme_t* color_scheme) {
  vc_t* vc = reinterpret_cast<vc_t*>(calloc(1, sizeof(vc_t)));
  if (!vc) {
    return ZX_ERR_NO_MEMORY;
  }
  vc->fd = -1;

  vc->keymap = qwerty_map;
  char* keys = getenv("virtcon.keymap");
  if (keys) {
    if (!strcmp(keys, "qwerty")) {
      vc->keymap = qwerty_map;
    } else if (!strcmp(keys, "dvorak")) {
      vc->keymap = dvorak_map;
    } else {
      printf("gfxconsole: no such keymap '%s'\n", keys);
    }
  }

  vc->font = vc_get_font();
  vc->charw = vc->font->width;
  vc->charh = vc->font->height;

  zx_status_t status = vc_setup(vc, color_scheme);
  if (status != ZX_OK) {
    free(vc);
    return status;
  }

  vc_attach_to_main_display(vc);
  vc_reset(vc);

  *out = vc;
  return ZX_OK;
}

void vc_free(vc_t* vc) {
  if (vc->fd >= 0) {
    close(vc->fd);
  }
  free(vc->text_buf);
  free(vc->scrollback_buf);
  free(vc);
}

void vc_flush(vc_t* vc) {
  if (g_vc_owns_display && vc_graphics_enabled(vc) && vc->invy1 >= 0) {
    int rows = vc_rows(vc);
    // Adjust for the current viewport position.  Convert
    // console-relative row numbers to screen-relative row numbers.
    int invalidate_y0 = MIN(vc->invy0 - vc->viewport_y, rows);
    int invalidate_y1 = MIN(vc->invy1 - vc->viewport_y, rows);
    vc_gfx_invalidate(vc->graphics, vc, 0, invalidate_y0, vc->columns,
                      invalidate_y1 - invalidate_y0);
  }
}

void vc_flush_all(vc_t* vc) {
  if (g_vc_owns_display && vc_graphics_enabled(vc)) {
    vc_gfx_invalidate_all(vc->graphics, vc);
  }
}
